﻿using System;
using Aeon.Emulator.Sound.FM.Channels;
using Aeon.Emulator.Sound.FM.Operators;

namespace Aeon.Emulator.Sound.FM
{
    /// <summary>
    /// Emulates an OPL3 FM synthesizer.
    /// </summary>
    public sealed class FmSynthesizer
    {
        private readonly byte[] registers = new byte[0x200];
        private readonly VibratoGenerator vibratoGenerator = new VibratoGenerator();
        private readonly TremoloGenerator tremoloGenerator;
        private readonly StandardOperator[,] operators = new StandardOperator[2, 0x20];
        private readonly Channel4[,] channels4op = new Channel4[2, 3];
        private readonly Channel[,] channels = new Channel[2, 9];
        private readonly BassDrum rhythmBassDrumChannel;
        private readonly RhythmChannel rhythmHighHatSnareDrumChannel;
        private readonly RhythmChannel rhythmTomTomTopCymbalChannel;
        private readonly double[] outputBuffer = new double[4];
        private readonly Channel2[,] channels2op = new Channel2[2, 9];
        private readonly SnareDrum rhythmSnareDrumOperator;
        private readonly StandardOperator rhythmTomTomOperator;
        private readonly StandardOperator highHatOperator;
        private readonly StandardOperator snareDrumOperator;
        private readonly StandardOperator tomTomOperator;
        private readonly StandardOperator topCymbalOperator;
        private bool rhythmModeEnabled;
        private int connectionMode;

        /// <summary>
        /// Initializes a new instance of the <see cref="FmSynthesizer"/> class.
        /// </summary>
        /// <param name="sampleRate">The sample rate.</param>
        public FmSynthesizer(int sampleRate)
        {
            this.SampleRate = sampleRate;
            this.tremoloGenerator = new TremoloGenerator(sampleRate);

            for(int a = 0; a < 2; a++)
            {
                for(int group = 0; group <= 0x10; group += 8)
                {
                    for(int offset = 0; offset < 6; offset++)
                    {
                        int baseAddress = (a << 8) | (group + offset);
                        operators[a, group + offset] = new StandardOperator(baseAddress, this);
                    }
                }

                for(int c = 0; c < 3; c++)
                {
                    int baseAddress = (a << 8) | c;
                    channels2op[a, c] = new Channel2(baseAddress, operators[a, c], operators[a, c + 0x3], this);
                    channels2op[a, c + 3] = new Channel2(baseAddress + 3, operators[a, c + 0x8], operators[a, c + 0xB], this);
                    channels2op[a, c + 6] = new Channel2(baseAddress + 6, operators[a, c + 0x10], operators[a, c + 0x13], this);
                }

                for(int c = 0; c < 9; c++)
                    channels[a, c] = channels2op[a, c];

                for(int c = 0; c < 3; c++)
                {
                    int baseAddress = (a << 8) | c;
                    channels4op[a, c] = new Channel4(baseAddress, operators[a, c], operators[a, c + 0x3], operators[a, c + 0x8], operators[a, c + 0xB], this);
                }
            }

            this.highHatOperator = operators[0, 0x11];
            this.snareDrumOperator = operators[0, 0x14];
            this.tomTomOperator = operators[0, 0x12];
            this.topCymbalOperator = operators[0, 0x15];

            this.RhythmHighHatOperator = new HighHat(this);
            this.rhythmTomTomOperator = new StandardOperator(0x12, this);
            this.RhythmTopCymbalOperator = new TopCymbal(this);
            this.rhythmBassDrumChannel = new BassDrum(this);
            this.rhythmSnareDrumOperator = new SnareDrum(this);
            this.rhythmHighHatSnareDrumChannel = new RhythmChannel(7, this.RhythmHighHatOperator, this.rhythmSnareDrumOperator, this);
            this.rhythmTomTomTopCymbalChannel = new RhythmChannel(8, this.rhythmTomTomOperator, this.RhythmTopCymbalOperator, this);
        }

        /// <summary>
        /// Gets the output sample rate of the OPL3 emulator.
        /// </summary>
        public int SampleRate { get; private set; }

        /// <summary>
        /// Gets the current vibrato multiplier.
        /// </summary>
        internal double VibratoMultiplier
        {
            get { return this.vibratoGenerator.CurrentValue; }
        }
        /// <summary>
        /// Gets the current tremolo multiplier.
        /// </summary>
        internal double TremoloMultiplier
        {
            get { return this.tremoloGenerator.CurrentValue; }
        }
        /// <summary>
        /// Gets the current note selection value.
        /// </summary>
        internal int NoteSelection { get; private set; }
        /// <summary>
        /// Gets the OPL3 register array.
        /// </summary>
        internal byte[] Registers
        {
            get { return this.registers; }
        }
        /// <summary>
        /// Gets the high hat operator.
        /// </summary>
        internal HighHat RhythmHighHatOperator { get; private set; }
        /// <summary>
        /// Gets the top cymbal operator.
        /// </summary>
        internal TopCymbal RhythmTopCymbalOperator { get; private set; }
        /// <summary>
        /// Gets the OPL3 mode index.
        /// </summary>
        internal int Opl3Mode { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether the OPL3 is in rhythm mode.
        /// </summary>
        private bool IsRhythmModeEnabled
        {
            get { return this.rhythmModeEnabled; }
            set
            {
                if(value != this.rhythmModeEnabled)
                {
                    this.rhythmModeEnabled = value;
                    
                    if(value)
                    {
                        this.channels[0, 6] = this.rhythmBassDrumChannel;
                        this.channels[0, 7] = this.rhythmHighHatSnareDrumChannel;
                        this.channels[0, 8] = this.rhythmTomTomTopCymbalChannel;
                        this.operators[0, 0x11] = this.RhythmHighHatOperator;
                        this.operators[0, 0x14] = this.rhythmSnareDrumOperator;
                        this.operators[0, 0x12] = this.rhythmTomTomOperator;
                        this.operators[0, 0x15] = this.RhythmTopCymbalOperator;
                    }
                    else
                    {
                        for(int i = 6; i <= 8; i++)
                            this.channels[0, i] = this.channels2op[0, i];

                        this.operators[0, 0x11] = this.highHatOperator;
                        this.operators[0, 0x14] = this.snareDrumOperator;
                        this.operators[0, 0x12] = this.tomTomOperator;
                        this.operators[0, 0x15] = this.topCymbalOperator;
                    }

                    for(int i = 6; i <= 8; i++)
                        this.channels[0, i].UpdateChannel();
                }
            }
        }

        /// <summary>
        /// Writes waveform data into a buffer.
        /// </summary>
        /// <param name="buffer">Buffer into which waveform data is written.</param>
        /// <param name="offset">Offset in buffer to start writing.</param>
        /// <param name="length">Length of data to write to the buffer.</param>
        public void GetData(short[] buffer, int offset, int length)
        {
            if(buffer == null)
                throw new ArgumentNullException("buffer");
            if(offset < 0 || offset >= buffer.Length)
                throw new ArgumentOutOfRangeException("Must be within bounds of buffer array.", "offset");

            for(int i = 0; i < length; i++)
                buffer[offset + i] = this.GetNextSample();
        }
        /// <summary>
        /// Writes an OPL3 register value.
        /// </summary>
        /// <param name="array">OPL3 register array.</param>
        /// <param name="address">OPL3 register address.</param>
        /// <param name="value">Value to write.</param>
        public void SetRegisterValue(int array, int address, byte value)
        {
            int registerAddress = (array << 8) | address;
            if(registerAddress < 0 || registerAddress >= 0x200)
                return;

            byte oldValue = this.registers[registerAddress];
            this.registers[registerAddress] = value;

            int regType = address & 0xE0;
            if(regType == 0)
            {
                if(array == 1)
                {
                    if(address == 0x04)
                    {
                        this.connectionMode = (this.registers[0x104] & 0x3F);
                        this.Enable4OperatorMode();
                    }
                    else if(address == 0x05)
                    {
                        Opl3Mode = this.registers[0x105] & 0x01;
                        if(Opl3Mode == 1)
                            EnableAllChannels();
                        this.Enable4OperatorMode();
                    }
                }
                else if(address == 0x08)
                {
                    this.NoteSelection = (this.registers[0x08] & 0x40) >> 6;
                }
            }
            else if(regType == 0xA0)
            {
                if(address == 0xBD)
                {
                    if(array == 0)
                        this.EffectsRegisterChanged(oldValue);
                }
                else if(((address & 0xF0) == 0xB0 && address <= 0xB8) || ((address & 0xF0) == 0xA0 && address <= 0xA8))
                    channels[array, address & 0x0F].UpdateChannel();
            }
            else if(regType == 0xC0)
            {
                if(address <= 0xC8)
                    channels[array, address & 0x0F].UpdateChannel();
            }
            else
            {
                var op = operators[array, address & 0x1F];
                if(op != null)
                    op.Recalculate();
            }
        }

        /// <summary>
        /// Returns the next sample of the output waveform.
        /// </summary>
        /// <returns>The next sample of the output waveform.</returns>
        private short GetNextSample()
        {
            for(int i = 0; i < outputBuffer.Length; i++)
                outputBuffer[i] = 0;

            for(int array = 0; array < (Opl3Mode + 1); array++)
            {
                for(int channelNumber = 0; channelNumber < 9; channelNumber++)
                    this.channels[array, channelNumber].AccumulateOutput(outputBuffer);
            }

            int sampleValue = 0;

            for(int i = 0; i < outputBuffer.Length; i++)
                sampleValue += (int)((outputBuffer[i] / 18.0) * 0x7FFF);

            this.vibratoGenerator.Increment();
            this.tremoloGenerator.Increment();

            return (short)sampleValue;
        }
        /// <summary>
        /// Updates values stored in the effects register.
        /// </summary>
        /// <param name="oldValue">The original register value.</param>
        private void EffectsRegisterChanged(byte oldValue)
        {
            byte value = registers[0xBD];
            
            this.tremoloGenerator.DAM = (value & 0x80) >> 7;
            this.vibratoGenerator.DVB = (value & 0x40) >> 6;
            this.IsRhythmModeEnabled = (value & 0x20) != 0;

            int diffs = (value ^ oldValue) & value;

            if((diffs & 0x10) != 0)
                this.rhythmBassDrumChannel.BassDrumKeyOn();

            if((diffs & 0x08) != 0)
                this.rhythmSnareDrumOperator.KeyOn();

            if((diffs & 0x04) != 0)
                this.rhythmTomTomOperator.KeyOn();

            if((diffs & 0x02) != 0)
                this.RhythmTopCymbalOperator.KeyOn();

            if((diffs & 0x01) != 0)
                this.RhythmHighHatOperator.KeyOn();
        }
        /// <summary>
        /// Enables all of the channels.
        /// </summary>
        private void EnableAllChannels()
        {
            for(int array = 0; array < 2; array++)
            {
                for(int i = 0; i < 9; i++)
                {
                    int baseAddress = channels[array, i].BaseAddress;
                    registers[baseAddress + 0xC0] |= 0xF0;
                    channels[array, i].UpdateChannel();
                }
            }
        }
        /// <summary>
        /// Sets channels to four-operator mode.
        /// </summary>
        private void Enable4OperatorMode()
        {
            for(int a = 0; a < 2; a++)
            {
                for(int i = 0; i < 3; i++)
                {
                    if(this.Opl3Mode == 1 && ((this.connectionMode >> (a * 3 + i)) & 0x01) != 0)
                    {
                        this.channels[a, i] = channels4op[a, i];
                        this.channels[a, i + 3] = NullChannel.Value;
                        this.channels[a, i].UpdateChannel();
                    }
                    else
                    {
                        this.channels[a, i] = channels2op[a, i];
                        this.channels[a, i + 3] = channels2op[a, i + 3];
                        this.channels[a, i].UpdateChannel();
                        this.channels[a, i + 3].UpdateChannel();
                    }
                }
            }
        }
    }
}
